//
//  CommonDelegate+Cocoa.swift
//  MacDemo
//
//  Created by FH on 2020/12/2.
//  Copyright © 2020 FH. All rights reserved.
//

import Foundation

class CommonObserver : NSObject {
    enum CallbackType {
        case notify
        case kvo
        case delegate
    }
    
    private let mutex = Mutex()
    private var delegateCallbacks: [String : [UUID: Action1<Any>]] = [:]
    private var notifyCallbacks: [String: [UUID: Action1<Notification>]] = [:]
    private var kvoCallbacks: [String : [UUID: Action2<String, Any>]] = [:]
    private var kvoRegister: [String : [Int : Set<String>]] = [:]
    
    func addObserver(groupName: String,
                     notifyCallbackFn: Action1<Notification>?=nil,
                     kvoCallbackFn: Action2<String, Any>?=nil,
                     delegateCallbackFn: Action1<Any>?=nil) -> UUID {
        let succ = mutex.tryLock()
        defer {
            if succ {
                mutex.unlock()
            }
        }
        let callbackId = UUID()
        if let callbackFn = notifyCallbackFn {
            var currCallbacks = self.notifyCallbacks[groupName]
            if currCallbacks == nil {
                currCallbacks = [UUID: Action1<Notification>]()
            }
            currCallbacks?[callbackId] = callbackFn
            self.notifyCallbacks[groupName] = currCallbacks!
        } else if let callbackFn = kvoCallbackFn {
            var currCallbacks = self.kvoCallbacks[groupName]
            if currCallbacks == nil {
                currCallbacks = [UUID: Action2<String, Any>]()
            }
            currCallbacks?[callbackId] = callbackFn
            self.kvoCallbacks[groupName] = currCallbacks!
        } else if let callbackFn = delegateCallbackFn {
            var currCallbacks = self.delegateCallbacks[groupName]
            if currCallbacks == nil {
                currCallbacks = [UUID: Action1<Any>]()
            }
            currCallbacks?[callbackId] = callbackFn
            self.delegateCallbacks[groupName] = currCallbacks!
        }
        return callbackId
    }
    
    func removeObserver(groupName: String, callbackId: UUID, callbackType: CallbackType) {
        let succ = mutex.tryLock()
        if callbackType == .notify {
            if var currCallbacks = self.notifyCallbacks[groupName] {
                currCallbacks.removeValue(forKey: callbackId)
                self.notifyCallbacks[groupName] = currCallbacks
            }
        } else if callbackType == .kvo {
            if var currCallbacks = self.kvoCallbacks[groupName] {
                currCallbacks.removeValue(forKey: callbackId)
                self.kvoCallbacks[groupName] = currCallbacks
            }
        } else if callbackType == .delegate {
            if var currCallbacks = self.delegateCallbacks[groupName] {
                currCallbacks.removeValue(forKey: callbackId)
                self.delegateCallbacks[groupName] = currCallbacks
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    // REMARK: menual trigger delegateCallback
    func onDelegateCallback(groupName: String, value: Any) {
        let succ = mutex.tryLock()
        if let currCallbacks = self.delegateCallbacks[groupName] {
            for (_, callback) in currCallbacks {
                callback(value)
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    // REMARK: notification observer callback
    
    @objc func onNotifyObserve(notify: Notification) {
        let succ = mutex.tryLock()
        if let currCallbacks = self.notifyCallbacks[notify.name.rawValue] {
            for (_, callback) in currCallbacks {
                callback(notify)
            }
        }
        if succ {
            mutex.unlock()
        }
    }
    
    // REMARK: kvo some methods
    
    func registerKVO(subject: NSObject, keyPath: String) {
        let typeName = String(describing: type(of: subject))
        let hashValue = subject.hashValue
        let succ = mutex.tryLock()
        var subjectMap = self.kvoRegister[typeName] ?? [Int : Set<String>]()
        var pathSet = subjectMap[hashValue] ?? Set<String>()
        if !pathSet.contains(keyPath) {
            subject.addObserver(self, forKeyPath: keyPath, options: .new, context: nil)
            
            pathSet.insert(keyPath)
            subjectMap[hashValue] = pathSet
            self.kvoRegister[typeName] = subjectMap
        }
        if succ {
            mutex.unlock()
        }
    }
    
    func unregisterKVO(typeName: String, subjectHashValue: Int) {
        let succ = mutex.tryLock()
        if var subjectMap = self.kvoRegister[typeName] {
            subjectMap.removeValue(forKey: subjectHashValue)
            self.kvoRegister[typeName] = subjectMap
        }
        if succ {
            mutex.unlock()
        }
    }
    
    override func observeValue(forKeyPath keyPath: String?,
                               of object: Any?,
                               change: [NSKeyValueChangeKey : Any]?,
                               context: UnsafeMutableRawPointer?) {
        if let objcInstance = object as? NSObject {
            let typeName = String(describing: type(of: objcInstance))
            let succ = mutex.tryLock()
            if let keyPath = keyPath, let callbacks = self.kvoCallbacks[typeName] {
                let value = objcInstance.value(forKeyPath: keyPath)
                for (_, callback) in callbacks {
                    callback(keyPath, value as Any)
                }
            }
            if succ {
                mutex.unlock()
            }
        }
    }
}

func sharedCommonNotification() -> CommonObserver {
    struct StaticInstance {
        static let instance = StaticInstance()
        let observer = CommonObserver()
    }
    return StaticInstance.instance.observer
}

func sharedCommonKVO() -> CommonObserver {
    struct StaticInstance {
        static let instance = StaticInstance()
        let observer = CommonObserver()
    }
    return StaticInstance.instance.observer
}

// target for response cocoa event or delegate
fileprivate var ProtocolSelectorMapping = [String : Set<String>]()
class CocoaCommonCallbacksTarget : ScriptEngineDispatch {
    let observer = CommonObserver()
    let methodName: String!
    let protocolType: Protocol?
    let protocolTypeName: String!
    var protocolSelectors: Set<String>!
    
    init(methodName: String) {
        self.methodName = methodName
        self.protocolType = nil
        self.protocolTypeName = nil
        self.protocolSelectors = nil
    }
    
    init(protocolType: Protocol) {
        self.methodName = nil
        self.protocolType = protocolType
        self.protocolTypeName = NSStringFromProtocol(protocolType)
        if let selectors = ProtocolSelectorMapping[self.protocolTypeName] {
            self.protocolSelectors = selectors
        } else {
            self.protocolSelectors = []
            self.getProtocolMethods(protocolType: protocolType)
            ProtocolSelectorMapping[self.protocolTypeName] = self.protocolSelectors
        }
    }

    // reflection protocol methods
    private func getProtocolMethods(protocolType: Protocol) {
        if NSStringFromProtocol(protocolType) == "NSObject" {
            return
        }
        var methodCount: UInt32 = 0
        if let methodList = protocol_copyMethodDescriptionList(protocolType, false, true, &methodCount) {
            for i in 0..<Int(methodCount) {
                let methodDesc = methodList[i];
                if let selector = methodDesc.name {
                    self.protocolSelectors.insert(NSStringFromSelector(selector))
                }
            }
            free(methodList)
        }
        var superProtocolCount: UInt32 = 0
        if let superProtocols = protocol_copyProtocolList(protocolType, &superProtocolCount) {
            for i in 0..<Int(superProtocolCount) {
                let superProtocol = superProtocols[i]
                getProtocolMethods(protocolType: superProtocol)
            }
        }
    }
    
    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                          isSDKInstance: Bool, instanceClass: String) {
        if let methodName = self.methodName, methodName == selector {
            self.observer.onDelegateCallback(groupName: selector, value: (selector, argValues))
        } else if let selectors = self.protocolSelectors, selectors.contains(selector) {
            self.observer.onDelegateCallback(groupName: self.protocolTypeName, value: (selector, argValues))
        }
    }
    
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                       isSDKInstance: Bool, instanceClass: String) { }
    
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], returnValue: Any,
                     isSDKInstance: Bool, instanceClass: String) { }

    func onCallback(selector: String, _ callbackFn: @escaping ([String : Any]) -> ()) -> UUID {
        assert(selector == self.methodName, "this target not responds \(selector)")
        return self.observer.addObserver(groupName: self.methodName, delegateCallbackFn: { val in
            if let arg = val as? (String, [String : Any]) {
                callbackFn(arg.1)
            }
        })
    }
    
    func onCallback(delegateProtocolType: String, _ callbackFn: @escaping ((String, [String : Any])) -> ()) -> UUID {
        assert(delegateProtocolType == self.protocolTypeName, "this target not responds \(delegateProtocolType)")
        return self.observer.addObserver(groupName: self.protocolTypeName, delegateCallbackFn: { val in
            if let arg = val as? (String, [String : Any]) {
                callbackFn(arg)
            }
        })
    }
    
    func removeCallback(callbackId: UUID) {
        let key = self.methodName ?? self.protocolTypeName!
        self.observer.removeObserver(groupName: key, callbackId: callbackId, callbackType: .delegate)
    }
}

func sharedCocoaCommonCallbacksTarget(selector: String?, protocolType: Protocol?) -> CocoaCommonCallbacksTarget {
    struct StaticInstance {
        static var instance = StaticInstance()
        var targets = [String : CocoaCommonCallbacksTarget]()
    }
    let key = selector ?? String(describing: protocolType)
    if let instance = StaticInstance.instance.targets[key] {
        return instance
    } else {
        var instance: CocoaCommonCallbacksTarget! = nil
        if let methodName = selector {
            instance = CocoaCommonCallbacksTarget(methodName: methodName)
        } else if let protocolType = protocolType {
            instance = CocoaCommonCallbacksTarget(protocolType: protocolType)
        }
        StaticInstance.instance.targets[key] = instance
        return instance
    }
}

func sharedCommonDelegateEngine() -> ScriptEngine {
    struct StaticInstance {
        static let instance = StaticInstance()
        let engine: ScriptEngine
        init() {
            // CocoaControlEvents
            let cocoaControlEvents = AstNode(type: .protocol, name: "CocoaControlEvents")
            let cocoaControlEventsTargetAction = AstNode(type: .method, name: "rac_eventTouch_targetAction:")
            cocoaControlEventsTargetAction.value = AstNodeValue()
            cocoaControlEventsTargetAction.declType = "void"
            cocoaControlEventsTargetAction.value?.valueType = "void"
            cocoaControlEventsTargetAction.value?.typeEncoding = "v@:@"
            
            let cocoaControlEventsTargetActionArg1 = AstNode(type: .parameter, name: "sender")
            cocoaControlEventsTargetActionArg1.declType = "NSObject"
            cocoaControlEventsTargetActionArg1.value = AstNodeValue()
            cocoaControlEventsTargetActionArg1.value?.valueType = "NSObject *"
            cocoaControlEventsTargetActionArg1.value?.typeEncoding = "@"
            
            cocoaControlEventsTargetAction.children.append(cocoaControlEventsTargetActionArg1)
            cocoaControlEvents.children.append(cocoaControlEventsTargetAction)
            
            // NSControlTextEditingDelegate
            let controlTextEditingDelegate = AstNode(type: .protocol, name: "NSControlTextEditingDelegate")
            let controlTextDidChangeMethod = AstNode(type: .method, name: "controlTextDidChange:")
            controlTextDidChangeMethod.value = AstNodeValue()
            controlTextDidChangeMethod.declType = "void"
            controlTextDidChangeMethod.value?.valueType = "void"
            controlTextDidChangeMethod.value?.typeEncoding = "v@:@"
            
            let controlTextDidChangeArg1 = AstNode(type: .parameter, name: "obj")
            controlTextDidChangeArg1.declType = "NSNotification"
            controlTextDidChangeArg1.value = AstNodeValue()
            controlTextDidChangeArg1.value?.valueType = "NSNotification *"
            controlTextDidChangeArg1.value?.typeEncoding = "@"
            
            controlTextDidChangeMethod.children.append(controlTextDidChangeArg1)
            controlTextEditingDelegate.children.append(controlTextDidChangeMethod)
            
            // create instance
            let delegateNames = ["CocoaControlEvents", "NSControlTextEditingDelegate"]
            let astNodes = [
                controlTextEditingDelegate.name: controlTextEditingDelegate,
                cocoaControlEvents.name: cocoaControlEvents
            ]
            engine = ScriptEngine(astNodes: astNodes,
                                  apiClassName: "",
                                  delegateClassNames: delegateNames)
            engine.setEngineName("CommonDelegate")
            engine.running()
        }
    }
    return StaticInstance.instance.engine
}

///////////////////////////////////////////////////////

protocol CocoaBindingDelegate {
    func updateValue(_ value: Any)
}

#if os(OSX)

extension NSTextField: CocoaBindingDelegate {
    func updateValue(_ value: Any) {
        let (valueType, valueStr) = getAnyValueStr(from: value)
        if valueStr == "nil" && valueType.starts(with: "Swift.Optional<") {
            return
        }
        if self.stringValue != valueStr {
            self.stringValue = valueStr
        }
    }
}

extension NSButton : CocoaBindingDelegate {
    func updateValue(_ value: Any) {
        if let select = self as? NSPopUpButton {
            var index: Int? = nil
            if let complexType = value as? ComplexCaseIterable {
                index = complexType.getCurrIndex()
            } else {
                let item = "\(value)"
                for i in 0..<select.itemTitles.count {
                    let title = select.itemTitles[i]
                    if title == item {
                        index = i
                        break
                    }
                }
            }
            if let index = index, index != select.indexOfSelectedItem {
                select.selectItem(at: index)
            }
        } else if let state = value as? Bool {
            let newValue: NSControl.StateValue = state ? .on : .off
            if self.state != newValue {
                self.state = newValue
            }
        }
    }
    
    func sendAction() {
        self.sendAction(self.action, to: self.target)
    }
}

#else
#endif
